// util, helper funcs - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

var util = {};

// get tbody cell(s) under thead cell
// if row==undefined, get all cell; otherwise, cell on that row index
// NOTE: does NOT work if any cell.rowSpan != 1
util.columnCells = function( th, rowIndex ) {
	// get absolute column for th
	for( var absCol=0, i=0; true; i++ ) {
		if( th.parentNode.cells[i] == th ) break;
		absCol += th.parentNode.cells[i].colSpan;
	}
	// look in tBody for cells; all rows or rowIndex
	var tBody = th.parentNode.parentNode.nextSibling;
	var cells = [];
	for( var r=((rowIndex==undefined)?0:rowIndex); true; r++ ) {
		if( rowIndex!==undefined && r>rowIndex ) break;
		if( rowIndex==undefined && r>=tBody.rows.length ) break;
		for( var c=0; true; c+=tBody.rows[r].cells[c].colSpan ) {
			if( c < absCol ) continue;
			if( c >= absCol+th.colSpan ) break;
			if( c >= absCol && c < absCol+th.colSpan ) cells.push(tBody.rows[r].cells[c]);
		}
	}
	return cells;
}

// helper - INPTUT type='time' does not handle single digit hr/mi
util.fixTime = function(str) {
	if( str == null || str == '' ) return '';
	
	var hour, minute, meridian, dt, matchArr;
	if( matchArr = str.match( /(\d+)\D+(\d+)(am|pm|)/i ) ) {
		hour = matchArr[1];
		minute = matchArr[2];
		meridian = matchArr[3].toLowerCase();
	}
	else if( matchArr = str.match( /(\d+)(am|pm|)/ ) ) {
		hour = matchArr[1];
		minute = '00';
		meridian = matchArr[2].toLowerCase();
	}
	if( meridian == 'pm' && parseInt(hour)<12 ) hour = parseInt(hour)+12;
	dt = new Date( '2001-01-01 '+hour+':'+minute );
	//-console.log( '2001-01-01 '+hour+':'+minute );
	if( isNaN(dt) ) {
		throw( "invalid Time: \""+str+"\"" );
		return '';
	}
	return dt.formatTime();
};
util.fixDate = function(str) {
	if( str == '' ) return '';

	var cur = new Date();	
	var year, month, day, dt, matchArr;
	if( matchArr = str.match( /(\d+)\D+(\d+)\D+(\d+)/ ) ) {
		year = matchArr[1];
		month = matchArr[2];
		day = matchArr[3];
	}
	else if( matchArr = str.match( /(\d+)\D+(\d+)/ ) ) {
		year = cur.getFullYear();
		month = matchArr[1];
		day = matchArr[2];
	}
	else if( matchArr = str.match( /(\d+)/ ) ) {
		year = cur.getFullYear();
		month = cur.getMonth()+1;
		day = matchArr[1];
	}
	if( parseInt(year)<1000 ) year = 2000+parseInt(year);
	//-console.log( '' + year + '-' + month + '-' + day );
	dt = new Date( '' + year + '-' + month + '-' + day );
	if( isNaN(dt) ) {
		throw( "invalid Date: \""+str+"\"" );
		return '';
	}
	return dt.formatDate();
};
util.fixNumber = function(str,precision) {
	var numb = parseFloat(str);
	if( isNaN(numb) ) throw( "Not a Number: "+numb );
	if( precision !== undefined ) numb = numb.toFixed(precision);
	// todo: meaningful numeric representation
	return numb;
};

// transform timestamp into standard
// todo: handle timezones by first bringing into Date etc
Date.prototype.formatDT = function() {
	return this.getFullYear()
		+'-'+( (this.getMonth()+1<10 )	? '0'+(this.getMonth()+1)	: this.getMonth()+1	)
		+'-'+( (this.getDate()<10 )	? '0'+this.getDate()		: this.getDate()	)
		+' '+( (this.getHours()<10 )	? '0'+this.getHours()		: this.getHours()	)
		+':'+( (this.getMinutes()<10 )	? '0'+this.getMinutes()		: this.getMinutes()	);
}
Date.prototype.formatDate = function() { return this.formatDT().substr(0,10); }
Date.prototype.formatTime = function() { return this.formatDT().substr(11); }

// get radio value from form (or document)
util.getRadio = function( form, itemName ) {

	// form: HTMLElement, string (name), or undefined (default to document)
	if( itemName==undefined ) {
		itemName = form;
		form = document;
	}
	if( typeof form == 'string' ) form = document.getElementById(form);

	// get list of radios, find checked and return
	Post.querySelectorAll("input[type='radio'][name='"+itemName+"']").forEach( function(el) {
		if( el.checked ) return el.value; } );
	return null;
}
// set radio value from form (or document)
util.setRadio = function( form, itemName, value ) {

	// form: HTMLElement, string (name), or undefined (default to document)
	if( value==undefined ) {
		value = itemName;
		itemName = form;
		form = document;
	}
	if( typeof form == 'string' ) form = document.getElementById(form);

	// get and set radio
	Post.querySelector("input[type='radio'][name='"+itemName.replace(':','\\:')+"']").forEach( function(el) {
		el.checked = (el.value == value ); } );
}

// nonblocking Alert
function nonblockAlert( msg ) {
	if( typeof msg != 'string' ) msg = msg.toString();
	msg = msg.replace(/"/g,'\\"');
	msg = msg.replace(/\n/g,"\\n");
	setTimeout( 'window.alert("'+msg+'")', 0 );
}
//
// add option to select
HTMLSelectElement.prototype.appendOption = function( value, label, select_value ) {
	var opt = document.createElement('option');
	opt.value = value;
	opt.appendChild( document.createTextNode( (label==undefined)?value:label ) );
	opt.checked = ( select_value != undefined && select_value == value );
	this.appendChild( opt );
}
// combine attribute values - separate by ';'
Element.prototype.combineAttribute = function( attr, val ) {
	if( this.hasAttribute(attr) ) {
		var vals = this.getAttribute(attr).split(' ');
		if( !vals.has(val) ) this.setAttribute(attr,this.getAttribute(attr)+';'+val);
	}
	else	this.setAttribute(attr,val);
}
// whether self or parent has attribute or attribute == val
Element.prototype.ancestorByAttribute = function( attr, val, myself ) {
	for( var node=(myself)?this:this.parentNode; node; node=node.parentNode ) {
		if( node.getAttribute(attr) != undefined &&( val == undefined || node.getAttribute(attr) == val )) return node;
		if( node.tagName == 'BODY' ) return null;
	}
	return undefined;
}
// get first instance of attribute in ancestor 
Element.prototype.ancestorGetAttribute = function( attr, myself ) {
	for( var node=(myself)?this:this.parentNode; node; node=node.parentNode ) {
		if( node.getAttribute(attr) != undefined ) return node.getAttribute(attr);
		if( node.tagName == 'BODY' ) return undefined;
	}
	return undefined;
}
// whether arg is ancestor of caller
Element.prototype.hasAncestor = function( obj, myself ) {
	for( var node=(myself)?this:this.parentNode; node; node=node.parentNode ) {
		if( node == obj ) return true;
		if( !node || node.tagName == 'BODY' ) return false;
	}
	return false;
}

/* deprecated?
HTMLCollection.prototype.indexOf = function( obj ) {
	for( var i=0; i<this.length; i++ ) {
		if (this[i]==obj) return i;
	}
	return -1;
}
*/

/* deprecated
// merge elements of arg into this
Element.prototype.merge = function( mergee ) {

	// cannot be self
	if( this == mergee ) throw( 'cannot merge with self' );

	// null mergee - nada
	if( !mergee ) return self;

	// pull elements from the front of merge into this
	// note: do not need to remove node from mergee - it is moved with the appendChild
	for( var i=0; mergee.firstChild && i<100; i++ ) this.appendChild(mergee.firstChild);

	// safety check
	if( i>=100 ) throw( 'did not pull all from mergee' );

	// remove mergee
	mergee.parentNode.removeChild( mergee );

	return self;
}
*/

/* deprecated?
// toggle style.display for this element
// todo: no longer needed under css3!
// arg display: true = <block>, false = 'none', undefined = <toggle>
Element.prototype.displayToggle = function( display ) {
	if( display == undefined ) display = ( this.style.display == 'none' );
	if( !display )				this.style.display = 'none';
	else if( this.tagName == 'DIV' )	this.style.display = 'block';
	else if( this.tagName == 'FORM' )	this.style.display = 'block';
	else if( this.tagName == 'TABLE' )	this.style.display = 'table';
	else if( this.tagName == 'TR' )		this.style.display = 'table-row';
	else if( this.tagName == 'TD' )		this.style.display = 'table-cell';
	else if( this.tagName == 'TH' )		this.style.display = 'table-cell';
	else					throw( 'displayToggle - not handling '+this.tagName );
}
*/

// Array.has == in_array
Array.prototype.has = function( element ) {
	return this.indexOf(element) > -1;
}

// Array Remove - By John Resig (MIT Licensed)
Array.prototype.remove = function( from, to ) {
	var rest = this.slice((to || from) + 1 || this.length);
	this.length = from < 0 ? this.length + from : from;
	return this.push.apply(this, rest);
};

// Array.unique - destructive, returns self
Array.prototype.unique = function() {
	return this.filter( function( el, idx, arr ) {
		for( var j=0; j<idx; j++ ) if( arr[j] === el ) return false;
		return true;
	} );
};

// getOwnPropertyNames - move to element
Object.prototype.forEachProperty = function(fn) { return Object.getOwnPropertyNames(this).forEach(fn); };
Object.prototype.everyProperty = function(fn) { return Object.getOwnPropertyNames(this).every(fn); };

// popup for new rcd
util.popupNew = function( h4, form, successFunc, failureFunc ) {

	// add buttons to form, show popup
	form.appendChild( E.div( E.button( add, 'Save' ), E.button( done, 'Cancel' ) ) );
	util.popup( h4, form );

	// funcs	
	function add( e ) {
		e.stopPropagation(); e.preventDefault();
		Post.update( {el:form, update:'form', request:'success'}, function( err ) {
			// success - relist skills
			if( !err ) {
				//-console.log( 'popupNew return success' );
				if( successFunc ) successFunc();
				util.popout();
			}
			// failure - show err
			else {
				//-console.log( 'popupNew return failure: '+err );
				if( failureFunc ) failureFunc();
				nonblockAlert( err );
			}
		} );
	};
	function done( e ) {
		e.stopPropagation(); e.preventDefault();
		util.popout();
	}
}

// modal popup
util.popup_dom = function() { return document.getElementById('popup'); };
util.popup = function( Eguts ) {

	// remove any popup or hover menus
	util.popout();
	
	document.body.appendChild( E.div( '#popup', Array.prototype.slice.call(arguments) ) );
	util.popup_dom().style.top = ( document.body.clientHeight - util.popup_dom().clientHeight ) / 3;
	document.body.appendChild( E.div( '#coverup' ) );

	// redo width
	util.popup_dom().style.left = ((document.body.clientWidth-util.popup_dom().offsetWidth)/2);

	// focus on first element
	var input = util.popup_dom().querySelector("INPUT[type='text'],INPUT[type='password'],TEXTAREA");
	if( input ) input.focus();
};
// popup that clears itself in ms
util.timedPopup = function( args, ms ) {
	util.popup( Array.prototype.slice.call(arguments,0,-1) );
	setTimeout( util.popout, Array.prototype.slice.call(arguments,-1)[0] );
};
// popout
util.popout = function( e ) {
	
	// if event driven, stop prop
	if( e instanceof Event ) { e.stopPropagation(); e.preventDefault(); }

	// remove coverup div
	//-console.log( 'coverup: '+document.getElementById('coverup') );
	if( document.getElementById('coverup') ) document.getElementById('coverup').parentNode.removeChild( document.getElementById('coverup') ); 

	// remove popup
	if( util.popup_dom() ) util.popup_dom().parentNode.removeChild( util.popup_dom() );
};

// hover menu - single instanced by definition
util.hover = {

	// state space: hover parent and hover element itself, curColor holder for cur's color to restore it
	cur:		null,
	menu:		null,
	curColor:	null,

	// create new hover and attach to parentNode
	init: function( cur, guts ) {

		// already where we want to be - we are re-initing from mouse moving from menu to cur
		if( cur == util.hover.cur ) return;

		// remove any hover menu if already existent
		util.hover.exit();

		// if disabled, nothing
		if( cur.ancestorByAttribute('disabled','true',true) ) return;

		// build new hover
		util.hover.cur = cur;
		util.hover.menu = E.div( '#hover', {'e.mouseout':util.hover.menuOut}, E.div( guts ) );
		util.hover.cur.appendChild(util.hover.menu);
		util.hover.cur.addEventListener('mouseout',util.hover.curOut,true);

		// change color of parent
		util.hover.curColor = cur.style.color;
		util.hover.cur.style.color = 'darkblue';
	},

	// mouseout for menu
	menuOut: function(e) {
		e.stopPropagation(); e.preventDefault();
		//-console.log( 'menuOut: target='+e.target+', related='+e.relatedTarget );
		if( e.relatedTarget && !e.relatedTarget.hasAncestor(util.hover.cur)
		&&  util.hover.menu && util.hover.menu.parentNode == util.hover.cur ) util.hover.exit();
	},
	// mouseout for cur
	curOut: function(e) {
		e.stopPropagation(); e.preventDefault();
		//-console.log( 'curOut: target='+e.target+', related='+e.relatedTarget );
		if( e.relatedTarget && !e.relatedTarget.hasAncestor(util.hover.cur)
		&&  util.hover.menu && util.hover.menu.parentNode == util.hover.cur ) util.hover.exit();
	},
	// remove any hover menu if already existent
	exit: function() {
		//-console.log( 'hover: exit ---' );
		var hvr = document.getElementById('hover');
		// nothing going on - make sure state space nulled out
		if( !hvr ) {
			util.hover.cur = null;
			util.hover.menu = null;
			return;
		}
		// state space squirreled
		if( !util.hover.cur || util.hover.menu!=hvr ) throw( 'util.hover state space incorrect' );
		// set back attributes for parentNode
		util.hover.cur.style.color = util.hover.curColor;
		util.hover.cur.removeChild( util.hover.menu );
		util.hover.cur.removeEventListener('mouseout',util.hover.curOut,true);
		util.hover.cur = null;
		util.hover.menu = null;
	},
};

// hover on this same as clicking (!)
util.autoclick = function(cur,ms) {
	var timer;
	if( !cur.hasAttribute('util.autoclick') ) {
		cur.addEventListener( 'mouseout', function(e) {
			//-console.log( 'autoclick out '+(e.relatedTarget == cur)+' '+cur.getAttribute('util.autoclick') );
			cur.setAttribute('util.autoclick','out');
			if( timer ) clearTimeout(timer); } );
	}
	//-console.log( 'autoclick in - '+cur.getAttribute('util.autoclick') );
	if( cur.getAttribute('util.autoclick') != 'in' || !timer ) {
		cur.setAttribute('util.autoclick','in');
		timer = setTimeout( function(e){ if( cur.getAttribute('util.autoclick')=='in' ) { cur.onclick(); } }, ms );
	}
}

// whether chrome - for html5 inputs
util.isChrome = function() { return( navigator.userAgent.toLowerCase().indexOf('chrome') > -1 ) };

// input type=number|date|time coerce change trigger
util.chrome = {
	inInput: false,
	clickCt: 0,
	input: function(e) {
		// fire only in orig form going down
		//-console.log( 'chrome.input' );
		//-console.log( 'e.target.pre='+Post.pre(e.target)+', current.pre='+e.currentTarget.getAttribute('rel') );
		if( Post.pre(e.target) != e.currentTarget.getAttribute('rel') ) return;
		//-console.log( 'chrome.input - real' );
		//-if( !(e.target instanceof HTMLInputElement && ['number','date','time'].has(e.target.type)) ) return;
		util.chrome.inInput = true;
	},
	mouseup: function(e) {
		// only interested in mouseup inside declared pre and on known columns
		if( !Post.col(e.target) || ( Post.pre(e.target) != e.currentTarget.getAttribute('rel') ) ) return;
		//-console.log( 'chrome.mouseup - real' );
		//-if( !(e.target instanceof HTMLInputElement && ['number','date','time'].has(e.target.type)) ) return;
		if( util.chrome.inInput ) {
			util.chrome.inInput = false;
			util.chrome.clickCt++;
			setTimeout( function() {
				util.chrome.clickCt--;
				if( util.chrome.clickCt ) return;
				console.log( 'mouseup call change' );
				// kludge: need to let natural onchange trigger keep from re-firing
				e.target.setAttribute('changeValue','input');
				var change = document.createEvent("Event");
				change.initEvent('change',true,true);
				e.target.dispatchEvent(change);
			}, 500 );
		}
	},
	change: function(e) {
		//-console.log( 'primedial change - changeValue='+e.target.getAttribute('changeValue')+', value='+e.target.value );
		// coming from input trigger
		if( e.target.getAttribute('changeValue')=='input' ) e.target.setAttribute('changeValue',e.target.value);
		// coming from secondary change trigger
		else if( e.target.hasAttribute('changeValue') && e.target.getAttribute('changeValue')==e.target.value ) {
			//-console.log( 'stop propagation' );
			e.stopPropagation();
			e.preventDefault();
		}
		// coming from raw change
		else e.target.removeAttribute('changeValue');
	}
}

// helper suite
var helper = {
	load: function(e) {
	},
	change: function(e) {

		console.log( 'helper called' );

		var el = (e instanceof Event) ? e.target : e;
		var str = Post.colValue(el);
		var fn = helper.util[el.type];
		if( fn ) {
			// call helper function according to input type
			console.log( 'calling fn' );
			try{ str = fn.call(el,str); }
			catch(err) { el.focus(); util.nonblockAlert(err); return; }
			// we're ok
			Post.colSetValue(el,str);
		}
		else	console.log( 'ALERT: no helper util function for '+el.type );

		// if this dep on another col, call it
		// todo: break recursion - here?
		helper.util.pntDep( el.getAttribute('depOn') );
		helper.util.childDep( el );

		// done successfully
		return true;
	},

	util: {
		// set our value from dependents
		pntDep: function(el) {

			switch( el.type ) {
			case 'datetime':
				// find date to use
				var tm = Post.querySiblingSelector( el, "[depOn='"+Post.col(el)+"'] [type='time']" );
				var dt = Post.querySiblingSelector( el, "[depOn='"+Post.col(el)+"'] [type='date']" );
				if( dt && Post.colValue(dt)=='' ) {
					Post.setValue(el,'');
					return;
				}
				if( dt ) dt = Post.colValue(dt);
				if( !dt && Post.colSiblingValue(el,ts).length >= 10 ) dt = Post.colSiblingValue(el,ts).substr(0,10);
				if( !dt ) dt = (new Date()).formatDate();

				// set timestamp
				Post.colSiblingSetValue(el,ts,dt+' '+Post.colValue(tm));
			}
		},

		childDep: function(el) {

			// set to dependents according to our value
			switch( el.type ) {
			case 'datetime':
				// find date to use
				var tm = Post.querySiblingSelector( el, "[depOn='"+Post.col(el)+"'] [type='time']" );
				var dt = Post.querySiblingSelector( el, "[depOn='"+Post.col(el)+"'] [type='date']" );
				var val = Post.colValue(el);
				if( dt ) Post.colSetValue(dt,((val)?val.substr(0,10):''));
				if( tm ) Post.colSetValue(tm,((val)?val.substr(11,5):''));
			}
		},
	},

	// mapping types to fix funcs
	time: util.fixTime,
	date: util.fixDate,
	datetime: util.fixDateTime,
	number: util.fixNumber
};

